package net.sf.fmj.ejmf.toolkit.media;

import javax.media.Clock;
import javax.media.ClockStartedError;
import javax.media.ClockStoppedException;
import javax.media.IncompatibleTimeBaseException;
import javax.media.Manager;
import javax.media.StopTimeSetError;
import javax.media.Time;
import javax.media.TimeBase;

/**
 * The AbstractClock provides an abstract implementation of the
 * javax.media.Clock interface. All methods are implemented except for
 * setStopTime() and getStopTime(). These methods will invariably require
 * implementation-specific functionality and therefore are left to the
 * subclasses to implement.
 * 
 * From the book: Essential JMF, Gordon, Talley (ISBN 0130801046). Used with
 * permission.
 * 
 * @see AbstractController
 * @see StopTimeMonitor
 * 
 * @author Steve Talley & Rob Gordon
 */
public class AbstractClock implements Clock {
	private TimeBase systemtimebase = Manager.getSystemTimeBase();
	private TimeBase timebase = systemtimebase;
	private Time mediaStartTime = new Time(0);
	private Time mediaStopTime = Clock.RESET;
	private Time timeBaseStartTime;
	private float rate = 1.0F;
	private boolean isStarted = false;

	/**
	 * Constructs an AbstractClock.
	 */
	public AbstractClock() {
		super();
	}

	// //////////////////////////////////////////////////////
	//
	// javax.media.Clock methods
	//
	// //////////////////////////////////////////////////////

	/**
	 * Set the <code>TimeBase</code> for this <code>Clock</code>. This method
	 * can only be called on a <i>Stopped</i> <code>Clock</code>. A
	 * <code>ClockStartedError</code> is thrown if <code>setTimeBase</code> is
	 * called on a <i>Started</i> <code>Clock</code>.
	 * <p>
	 * A <code>Clock</code> has a default <code>TimeBase</code> that is
	 * determined by the implementation. To reset a <code>Clock</code> to its
	 * default <code>TimeBase</code>, call <code>setTimeBase(null)</code>.
	 * 
	 * @param timebase
	 *            The new <CODE>TimeBase</CODE> or <CODE>null</CODE> to reset
	 *            the <code>Clock</code> to its default <code>TimeBase</code>.
	 * 
	 * @exception IncompatibleTimeBaseException
	 *                Thrown if the <code>Clock</code> can't use the specified
	 *                <code>TimeBase</code>.
	 */
	public synchronized void setTimeBase(TimeBase timebase)
			throws IncompatibleTimeBaseException {
		if (isStarted) {
			throw new ClockStartedError(
					"Cannot set time base on a Started Clock");
		}

		if (timebase == null) {
			this.timebase = systemtimebase;
		} else {
			this.timebase = timebase;
		}
	}

	/**
	 * Get the TimeBase that this Clock is using.
	 */
	public synchronized TimeBase getTimeBase() {
		return timebase;
	}

	/**
	 * Sets the media time.
	 * 
	 * @param t
	 *            The media time to set
	 * 
	 * @exception ClockStartedError
	 *                If the Clock is Started.
	 */
	public synchronized void setMediaTime(Time t) {
		if (isStarted) {
			throw new ClockStartedError(
					"Cannot set media time on a Started Clock");
		}

		mediaStartTime = t;
	}

	/**
	 * Get the media time the media is scheduled to start (if the Clock is
	 * stopped), or the media time at which the Clock started (if the Clock is
	 * started).
	 */
	protected Time getMediaStartTime() {
		return mediaStartTime;
	}

	/**
	 * Get the time-base time the media is scheduled to start (if the Clock is
	 * stopped), or the time-base time at which the Clock started (if the Clock
	 * is started).
	 */
	protected Time getTimeBaseStartTime() {
		return timeBaseStartTime;
	}

	/**
	 * Calculates the current media time based on the current time-base time,
	 * the time-base start time, the media start time, and the rate.
	 * 
	 * @return The current media time
	 */
	public synchronized Time getMediaTime() {
		if (!isStarted) {
			// If the Clock is stopped return it's starting
			// media-time
			return mediaStartTime;
		} else {
			// Calculate the media time
			return calculateMediaTime();
		}
	}

	/**
	 * Get the media time in nanoseconds.
	 */
	public synchronized long getMediaNanoseconds() {
		return getMediaTime().getNanoseconds();
	}

	/**
	 * Calculates the media time on a started Clock based on the assumption that
	 * the Clock is started.
	 */
	private synchronized Time calculateMediaTime() {
		long tbCurrent = timebase.getNanoseconds();
		long tbStart = timeBaseStartTime.getNanoseconds();

		// If we are scheduled to start but haven't yet reached
		// the scheduled start time, return the media start time
		if (tbCurrent < tbStart) {
			return mediaStartTime;
		}

		long mStart = mediaStartTime.getNanoseconds();
		long mCurrent = (long) ((tbCurrent - tbStart) * rate + mStart);

		return new Time(mCurrent);
	}

	/**
	 * Gets the time until the Clock's media synchronizes with its time-base.
	 * 
	 * @return The time remaining until the time-base start-time if this Clock
	 *         is Started and the time-base start-time has not yet been reached,
	 *         or the media time otherwise.
	 */
	public synchronized Time getSyncTime() {
		if (isStarted) {
			long startNano = timeBaseStartTime.getNanoseconds();
			long nowNano = getTimeBase().getNanoseconds();

			if (startNano >= nowNano) {
				return new Time((long) (nowNano - startNano));
			}
		}

		return getMediaTime();
	}

	/**
	 * Given a media time, returns the corresponding time-base time. Uses the
	 * current rate, the time-base start time, and the media start time to
	 * calculate.
	 * 
	 * @param t
	 *            A media time to be mapped to a time-base time.
	 * 
	 * @return A time-base time.
	 * 
	 * @exception ClockStoppedException
	 *                If the clock has not started.
	 */
	public synchronized Time mapToTimeBase(Time t) throws ClockStoppedException {
		if (!isStarted) {
			throw new ClockStoppedException(
					"Cannot map media time to time-base time on a Stopped Clock");
		}

		long mCurrent = t.getNanoseconds();
		long mStart = mediaStartTime.getNanoseconds();
		long tbStart = timeBaseStartTime.getNanoseconds();

		return new Time((long) (((mCurrent - mStart) / rate) + tbStart));
	}

	/**
	 * Set the temporal scale factor. The argument <i>suggests</i> the scale
	 * factor to use.
	 * <p>
	 * The <code>setRate</code> method returns the actual rate set by the
	 * <code>Clock</code>. <code>Clocks</code> should set their rate as close to
	 * the requested value as possible, but are not required to set the rate to
	 * the exact value of any argument other than 1.0. A <code>Clock</code> is
	 * only guaranteed to set its rate exactly to 1.0.
	 * 
	 * @param rate
	 *            The temporal scale factor (rate) to set.
	 * 
	 * @exception ClockStartedError
	 *                If the Clock is Started.
	 * 
	 * @return The actual rate set.
	 * 
	 */
	public synchronized float setRate(float rate) {
		if (isStarted) {
			throw new ClockStartedError("Cannot set rate on a Started Clock");
		}

		if (rate != 0.0F) {
			this.rate = rate;
		}

		return this.rate;
	}

	/**
	 * Get the current temporal scale factor. The scale factor defines the
	 * relationship between the <code>Clock's</code> <i>media time</i> and its
	 * <code>TimeBase</code>.
	 * <p>
	 * For example, a rate of 2.0 indicates that <i>media time</i> will pass
	 * twice as fast as the <code>TimeBase</code> time once the
	 * <code>Clock</code> starts. Similarly, a negative rate indicates that the
	 * <code>Clock</code> runs in the opposite direction of its
	 * <code>TimeBase</code>. All <code>Clocks</code> are guaranteed to support
	 * a rate of 1.0, the default rate. <code>Clocks</code> are not required to
	 * support any other rate.
	 * <p>
	 */
	public synchronized float getRate() {
		return rate;
	}

	/**
	 * Set the <i>media time</i> at which you want the <code>Clock</code> to
	 * stop. The <code>Clock</code> will stop when its <i>media time</i> passes
	 * the stop-time. To clear the stop time, set it to:
	 * <code>Clock.RESET</code>.
	 * <p>
	 * You can always call <code>setStopTime</code> on a <i>Stopped</i>
	 * <code>Clock</code>.
	 * <p>
	 * On a <i>Started</i> <code>Clock</code>, the stop-time can only be set
	 * <I>once</I>. A <code>StopTimeSetError</code> is thrown if
	 * <code>setStopTime</code> is called and the <i>media stop-time</i> has
	 * already been set.
	 * 
	 * @param mediaStopTime
	 *            The time at which you want the <code>Clock</code> to stop, in
	 *            <i>media time</i>.
	 */
	public synchronized void setStopTime(Time mediaStopTime) {
		if (isStarted && this.mediaStopTime != RESET) {
			throw new StopTimeSetError(
					"Stop time may be set only once on a Started Clock");
		}

		this.mediaStopTime = mediaStopTime;
	}

	/**
	 * Get the last value successfully set by setStopTime. Returns the constant
	 * Clock.RESET if no stop time is set
	 * 
	 * @return The current stop time.
	 */
	public synchronized Time getStopTime() {
		return mediaStopTime;
	}

	/**
	 * syncStart the AbstractClock at the previously- specified time-base start
	 * time.
	 * <p>
	 * Synchronous method -- return when transition complete
	 */
	public synchronized void syncStart(Time t) {

		// Enforce state prereqs
		if (isStarted) {
			throw new ClockStartedError(
					"syncStart() cannot be called on a started Clock");
		}

		long now = getTimeBase().getNanoseconds();
		long start = t.getNanoseconds();

		if (start - now > 0) {

			// Start time is in the future

			// Set the time-base start time
			this.timeBaseStartTime = new Time(start);

		} else {

			// Start time is in the past

			// Set the time-base start time to be now
			this.timeBaseStartTime = new Time(now);
		}

		isStarted = true;
	}

	/**
	 * Stop the Clock.
	 */
	public synchronized void stop() {
		if (isStarted) {
			mediaStartTime = calculateMediaTime(); // KAL: TODO: what is the
													// reason for this?
			// Commented out -- is this necessary?
			// timeBaseStartTime = timebase.getTime();
			isStarted = false;
		}
	}
}
